﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using RoboCoder.GameState;
using RobozzleModel;
using System.Diagnostics;
using System.ServiceModel.Channels;
using System.Data.SqlClient;
using System.Data;
using System.Transactions;
using System.Web.Security;
using System.Security.Cryptography;
using System.Data.Common;
using System.Data.EntityClient;
using System.Web.Hosting;
using System.ServiceModel.Activation;

// NOTE: If you change the class name "RobozzleService" here, you must also update the reference to "RobozzleService" in Web.config.
public class RobozzleService : IRobozzleService
{
    public const int ROWS = 12, COLS = 16, MAX_SUBS = 5, MAX_SUB_LEN = 10;
    public const int PUZZLE_THREAD_ID = 1;
    public const int MAX_COMMANDS = 1000;

    // Values for LevelInfo.AllowedCommands.
    public const int ALLOWED_PAINT_RED = 0x01;
    public const int ALLOWED_PAINT_GREEN = 0x02;
    public const int ALLOWED_PAINT_BLUE = 0x04;
    public const int ALLOWED_INVALID = 0x08;

    // Values for LevelInfo.RobotDir.
    public const int DIRECTION_RIGHT = 0;
    public const int DIRECTION_DOWN = 1;
    public const int DIRECTION_LEFT = 2;
    public const int DIRECTION_UP = 3;
    public const int DIRECTION_INVALID = 4;

    // Values for characters in LevelInfo.Colors.
    public const char COLOR_RED = 'R';
    public const char COLOR_GREEN = 'G';
    public const char COLOR_BLUE = 'B';
    public const char COLOR_NONE = '_';

    // Values for characters in LevelInfo.Items.
    public const char ITEM_HOLE = '#';
    public const char ITEM_FLOOR = '.';
    public const char ITEM_STAR = '*';

    public LevelInfo[] GetLevels(int levelId, out Dictionary<int,int> commentCounts)
    {
        RobozzleEntities entities = new RobozzleEntities();
        List<LevelInfo> levels = new List<LevelInfo>();
        IQueryable<LevelWithDifficulty> levelEntities = entities.LevelWithDifficulty;

        if (levelId != -1)
        {
            levelEntities = levelEntities.Where(L => L.Id == levelId);
        }
        else
        {
            levelEntities = levelEntities.Where(L => L.CoolnessVoteSum > StatsCommon.BAD_PUZZLE_THRESHOLD);
        }

        foreach (var levelEntity in levelEntities)
        {
            string[] colors, items;
            EntityToObj(levelEntity.Board, out colors, out items);

            int liked = (levelEntity.CoolnessVoteSum + levelEntity.CoolnessVoteCount) / 2;
            int disliked = levelEntity.CoolnessVoteCount - liked;

            LevelInfo level = new LevelInfo
            {
                Colors = colors,
                Items = items,
                RobotRow = levelEntity.RobotRow,
                RobotCol = levelEntity.RobotCol,
                RobotDir = levelEntity.RobotDir,
                Title = levelEntity.Title,
                About = levelEntity.About,
                SubmittedBy = levelEntity.SubmittedBy,
                SubmittedDate = levelEntity.SubmittedDate,
                SubLengths = IntToSubs(levelEntity.Subs),
                Id = levelEntity.Id,
                Solutions = levelEntity.Solutions,
                DifficultyVoteCount = levelEntity.DifficultyVoteCount,
                DifficultyVoteSum = levelEntity.DifficultyVoteSum,
                Liked = liked,
                Disliked = disliked,
                Featured = levelEntity.Featured,
                AllowedCommands = levelEntity.AllowedCommands,
            };
            levels.Add(level);
        }

        // Find the comment count
        commentCounts = new Dictionary<int, int>();
        foreach (var pair in entities.ForumThread
            .Where(thread => thread.PuzzleId != null)
            .Select(thread => new { PuzzleId = thread.PuzzleId, CommentCount = thread.ForumComment.Count }))
        {
            commentCounts[(int)pair.PuzzleId] = pair.CommentCount;
        }

        return levels.ToArray();
    }

    public LevelInfo[] GetLevels2(out Dictionary<int, int> commentCounts)
    {
        return GetLevels(-1, out commentCounts);
    }

    public string SubmitLevel(LevelInfo level, string userName, string pwd, string solution, out int levelId)
    {
        levelId = -1000;
        try
        {
            if (!IsValidLevelInfo(level))
                return "Invalid level";

            using (RobozzleEntities entities = new RobozzleEntities())
            {
                if (userName != null && !CheckAccess(userName, pwd, entities)) return "Bad credentials.";

                Level levelEntity = new Level
                {
                    Title = level.Title,
                    About = level.About,
                    Board = ObjToEntity(level.Colors, level.Items),
                    RobotRow = level.RobotRow,
                    RobotCol = level.RobotCol,
                    RobotDir = level.RobotDir,
                    Subs = SubsToInt(level.SubLengths),
                    SubmittedDate = DateTime.Now,
                    SubmittedBy = userName,
                    AllowedCommands = level.AllowedCommands,
                };

                if (!verifySolution(levelEntity, solution))
                    return "Invalid solution.";

                entities.AddToLevel(levelEntity);
                entities.SaveChanges();

                entities.AddToForumThread(new ForumThread
                {
                    ThreadTitle = level.Title,
                    PuzzleId = levelEntity.Id,
                    DateOpened = DateTime.Now,
                    IpAddress = "127.0.0.1",
                    OpenedBy = userName ?? "igoro",
                    ForumCategory = entities.ForumCategory.Where(cat => cat.Id == PUZZLE_THREAD_ID).First()
                });

                entities.SaveChanges();
                levelId = levelEntity.Id;
                
                string returnValue = SubmitSolution(levelId, userName, pwd, solution);

                GenerateThumb(levelEntity);

                return returnValue;
            }
        }
        catch (Exception e)
        {
            return e.ToString();
        }
    }

    private void GenerateThumb(Level levelEntity)
    {
        try
        {
            OperationContext oc = OperationContext.Current;
            VirtualPathExtension extension = oc.Host.Extensions.Find<VirtualPathExtension>();
            string path = HostingEnvironment.MapPath(extension.VirtualPath + "\\..");

            ThumbnailGenerator.Generate(levelEntity, path);
        }
        catch (Exception e)
        {
            // Badness ... but oh well
            if (e is StackOverflowException || e is OutOfMemoryException) throw;
        }
    }

    public bool LogIn(string userName, string password, out int[] solvedLevels, out LevelVoteInfo[] votes)
    {
        RobozzleEntities entities = new RobozzleEntities();
        if (CheckAccess(userName, password, entities))
        {
            solvedLevels = entities.Solution.Where(sol => sol.UserName == userName).Select(sol => sol.LevelId)
                .Distinct().OrderBy(x => x).ToArray();
            votes = entities.LevelVote
                .Where(vote => vote.UserName == userName)
                .AsEnumerable()
                .Select(vote => new LevelVoteInfo(vote.UserName, vote.LevelId, vote.VoteKind, vote.Vote))
                .ToArray();
            return true;
        }
        else
        {
            solvedLevels = null;
            votes = null;
            return false;
        }
    }

    private bool CheckAccess(string userName, string password, RobozzleEntities entities)
    {
        return entities.User.Any(user => user.Username == userName && user.Password == password);
    }

    public string SubmitSolution(int levelId, string userName, string password, string solution)
    {
        try
        {
            RobozzleEntities entities = new RobozzleEntities();
            if (userName != null && !CheckAccess(userName, password, entities)) return null;

            return SubmitSolution(levelId, userName, solution, entities);
        }
        catch (Exception e)
        {
            return e.ToString();
        }
    }

    public string SubmitSolution(int levelId, string userName, string solution, RobozzleEntities entities)
    {
        Solution soln = new Solution { LevelId = levelId, UserName = userName, Solution1 = solution, Date = DateTime.Now };

        if (levelId > 0)
        {
            Level levelEntity = entities.Level.Where(L => L.Id == levelId).First();
            if (!verifySolution(levelEntity, solution))
                return "Invalid solution.";

            levelEntity.Solutions = levelEntity.Solutions + 1;
        }

        entities.AddToSolution(soln);
        entities.SaveChanges();

        return null;
    }

    private bool verifySolution(Level level, string solution)
    {
        string[][] commands = ParseSolution(solution);
        if (!IsValidProgram(level, commands))
            return false;

        return new SolutionVerifier().IsSolution(level, commands);
    }

    /// <summary>
    /// This checks whether a program is valid for the level.
    /// A valid program is any program that can be constructed using the official client.
    /// This function does not test if the program solves the level.
    /// </summary>
    private bool IsValidProgram(Level level, string[][] commands)
    {
        int[] subLengths = IntToSubs(level.Subs);

        // Check that the solution contains the right number of functions.
        if (commands.Length != subLengths.Length)
            return false;

        // Check that all the functions fit.
        for (int i = 0; i < commands.Length; ++i)
            if (commands[i].Length > subLengths[i])
                return false;

        // Check that commands used in the solution are allowed.
        bool allowPaintRed = (level.AllowedCommands & ALLOWED_PAINT_RED) != 0;
        bool allowPaintGreen = (level.AllowedCommands & ALLOWED_PAINT_GREEN) != 0;
        bool allowPaintBlue = (level.AllowedCommands & ALLOWED_PAINT_BLUE) != 0;
        foreach (string[] function in commands)
        {
            foreach (string command in function)
            {
                char commandType = command[1];
                if (commandType == 'r' && !allowPaintRed) return false;
                if (commandType == 'g' && !allowPaintGreen) return false;
                if (commandType == 'b' && !allowPaintBlue) return false;
            }
        }

        return true;
    }

    private static string[][] ParseSolution(string solution)
    {
        // Split the solution into functions.
        string[] functions = solution.Split('|').Take(MAX_SUBS).ToArray();

        // Split the functions into individual commands.
        // Convert the color conditionals to upper case.
        string[][] commands = new string[functions.Length][];
        for (int i = 0; i < commands.Length; ++i)
        {
            string f = functions[i];
            commands[i] = new string[f.Length / 2];
            for (int j = 0; j < f.Length / 2; j++)
                commands[i][j] = f[j * 2].ToString().ToUpper() + f[j * 2 + 1];

        }
        return commands;
    }

    /// <summary>
    /// Check that a level submitted by a user is in the correct format.
    /// This function does not test if the level is solvable.
    /// </summary>
    private bool IsValidLevelInfo(LevelInfo levelInfo)
    {
        // Check SubLengths.
        if (levelInfo.SubLengths.Length != MAX_SUBS)
            return false;

        foreach (int l in levelInfo.SubLengths)
            if (l < 0 || l > MAX_SUB_LEN) return false;

        // Check Colors.
        if (levelInfo.Colors.Length != ROWS)
            return false;

        foreach (string row in levelInfo.Colors)
        {
            if (row.Length != COLS)
                return false;
            foreach (char color in row)
            {
                if (color != COLOR_RED && color != COLOR_BLUE && color != COLOR_GREEN)
                    return false;
            }
        }

        // Check Items.
        if (levelInfo.Items.Length != ROWS)
            return false;

        foreach (string row in levelInfo.Items)
        {
            if (row.Length != COLS)
                return false;
            foreach (char item in row)
            {
                if (item != ITEM_HOLE && item != ITEM_FLOOR && item != ITEM_STAR)
                    return false;
            }
        }

        // Check Title.
        if (string.IsNullOrEmpty(levelInfo.Title)) return false;

        // Check About.
        // (No checks.)

        // Check RobotRow, RobotCol and RobotDir.
        if (levelInfo.RobotRow < 0 || levelInfo.RobotRow >= ROWS) return false;
        if (levelInfo.RobotCol < 0 || levelInfo.RobotCol >= COLS) return false;
        if (levelInfo.RobotDir < 0 || levelInfo.RobotDir >= DIRECTION_INVALID) return false;

        // Check allowed commands.
        if (levelInfo.AllowedCommands < 0 || levelInfo.AllowedCommands >= ALLOWED_INVALID) return false;

        return true;
    }

    public string RegisterUser(string userName, string password, string email, int[] solvedLevels)
    {
        return RegisterUser2(userName, password, email, solvedLevels.ToDictionary(x => x, x => (string)null));
    }

    public string RegisterUser2(string userName, string password, string email, Dictionary<int,string> solvedLevels)
    {
        // Always treat useryName as lower-case
        userName = userName.ToLower();

        using (RobozzleEntities entities = new RobozzleEntities())
        {
            try
            {
                entities.AddToUser(new User { Username = userName, Password = password, Email = email });
                entities.SaveChanges();
            }
            catch (UpdateException)
            {
                return "User with this name already exists.";
            }
            catch (Exception)
            {
                return "Internal error occured.";
            }

            DateTime now = DateTime.Now;
            foreach (var levelRow in solvedLevels)
            {
                SubmitSolution(levelRow.Key, userName, password, levelRow.Value);
            }
            entities.SaveChanges();

            return null;
        }
    }

    public void GetTopSolvers(out string[] names, out int[] solved)
    {
        RobozzleEntities entities = new RobozzleEntities();
        TopSolver[] top = entities.TopSolver.OrderByDescending(t => t.LevelsSolved).ThenBy(t=> t.UserName).Take(10).ToArray();

        names = top.Select(entry => entry.UserName).ToArray();
        solved = top.Select(entry => entry.LevelsSolved).ToArray();
    }

    public void GetTopSolvers2(out string[] names, out int[] solved, out string[] namesToday, out int[] solvedToday)
    {
        RobozzleEntities entities = new RobozzleEntities();

        TopSolver[] top = entities.TopSolver.OrderByDescending(t => t.LevelsSolved).ThenBy(t => t.UserName).Take(10).ToArray();
        names = top.Select(entry => entry.UserName).ToArray();
        solved = top.Select(entry => entry.LevelsSolved).ToArray();

        TopSolverToday[] topToday = entities.TopSolverToday.OrderByDescending(t => t.LevelsSolved).ThenBy(t => t.UserName).Take(10).ToArray();
        namesToday = topToday.Select(entry => entry.UserName).ToArray();
        solvedToday = topToday.Select(entry => entry.LevelsSolved).ToArray();
    }

    public void Internal()
    {
    }


    public void SubmitLevelVote(string userName, string password, int levelId, int vote0, int vote1)
    {
        using (RobozzleEntities entities = new RobozzleEntities())
        {
            if (!CheckAccess(userName, password, entities)) return;

            DbConnection store = ((EntityConnection)entities.Connection).StoreConnection;
            store.Open();
            if (IsValidVote(vote0))
            {
                using (SqlCommand cmd = (SqlCommand)(store.CreateCommand()))
                {
                    cmd.CommandType = CommandType.StoredProcedure;
                    cmd.CommandText = "vote";
                    cmd.Parameters.AddWithValue("@userName", userName);
                    cmd.Parameters.AddWithValue("@levelId", levelId);
                    cmd.Parameters.AddWithValue("@vote", vote0);
                    cmd.Parameters.AddWithValue("@voteKind", 0);
                    cmd.ExecuteNonQuery();
                }
            }

            if (IsValidVote(vote1))
            {
                using (SqlCommand cmd = (SqlCommand)(store.CreateCommand()))
                {
                    cmd.CommandType = CommandType.StoredProcedure;
                    cmd.CommandText = "vote";
                    cmd.Parameters.AddWithValue("@userName", userName);
                    cmd.Parameters.AddWithValue("@levelId", levelId);
                    cmd.Parameters.AddWithValue("@vote", vote1);
                    cmd.Parameters.AddWithValue("@voteKind", 1);
                    cmd.ExecuteNonQuery();
                }
            }
            store.Close();
        }
    }

    public void SubmitComment(string userName, string password, int puzzleId, string comment, string ipAddress)
    {
        if (comment == "" || comment == null) return;
        RobozzleEntities entities = new RobozzleEntities();
        if (userName != null && !CheckAccess(userName, password, entities)) throw new InvalidOperationException("Bad credentials");

        ForumThread thread = entities.ForumThread.Where(t => t.PuzzleId == puzzleId).FirstOrDefault();
        if (thread == null) throw new InvalidOperationException("Invalid thread ID");

        entities.AddToForumComment(
            new ForumComment
            {
                Comment = comment,
                UserName = userName,
                Date = DateTime.Now,
                IpAddress = ipAddress,
                ForumThread = thread
            });
        entities.SaveChanges();
    }

    /// <summary>
    /// We'll use big negative numbers for "invalid" votes
    /// </summary>
    private bool IsValidVote(int vote)
    {
        return vote >= -100;
    }


    // What the heck? Is this even used for anything?
    public void GetLevelVotes(out int[] votes, out int[] kinds)
    {
        using (RobozzleEntities entities = new RobozzleEntities())
        {
            LevelVote[] a = entities.LevelVote.ToArray();
            votes = new int[a.Length];
            kinds = new int[a.Length];
            for (int i = 0; i < a.Length; i++)
            {
                votes[i] = a[i].Vote;
                kinds[i] = a[i].VoteKind;
            }
        }
    }

    #region Helper methods for levels
    public static int SubsToInt(int[] subs)
    {
        if (subs.Length != MAX_SUBS) throw new InvalidOperationException();
        int ret = 0;
        for (int i = 0, f = 1; i < 5; i++, f *= (MAX_SUB_LEN+1))
        {
            ret += f * subs[i];
        }
        return ret;
    }

    public static int[] IntToSubs(int x)
    {
        int[] subs = new int[MAX_SUBS];
        for (int i = 0; i < 5; i++)
        {
            subs[i] = x % (MAX_SUB_LEN + 1);
            x /= (MAX_SUB_LEN + 1);
        }
        return subs;
    }

    public static void EntityToObj(string board, out string[] colors, out string[] items)
    {
        colors = new string[ROWS];
        items = new string[ROWS];

        for (int r = 0; r < ROWS; r++) {
            char[] itemRow = new char[COLS];
            char[] colorRow = new char[COLS];

            for (int c = 0; c < COLS; c++)
            {
                char ch = board[COLS * r + c];
                if (ch == '#') { itemRow[c] = '#'; colorRow[c] = 'R'; }
                else
                {
                    colorRow[c] = Char.ToUpper(ch);
                    itemRow[c] = Char.IsUpper(ch) ? '*' : '.';
                }
            }

            colors[r] = new string(colorRow);
            items[r] = new string(itemRow);
        }
    }

    public static string ObjToEntity(string[] colors, string[] items)
    {
        int rows = colors.Length, cols = colors[0].Length;
        if (rows != ROWS) throw new InvalidOperationException();
        if (cols != COLS) throw new InvalidOperationException();

        char[] board = new char[rows * cols];
        for (int r = 0; r < rows; r++) for (int c = 0; c < cols; c++)
            {
                board[cols * r + c] = Combine(colors[r][c], items[r][c]);
            }
        return new string(board);
    }

    private static char Combine(char color, char item)
    {
        if (item == '#') return '#';
        if (item == '*') return Char.ToUpper(color);
        return Char.ToLower(color);
    }
    #endregion
}
